#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Side-Channel Attack实验流水线ExecuteScript
According toJSONConfigurationFile自动ExecuteComplete的实验流程：
1. Trace采集 (collect_firmware_traces.py)
2. Trace预Process (preprocess_traces.py, Optional)
3. AttackAnalyze (analyze_firmware_traces.py)
4. Result汇总
"""

import os
import sys
import json
import argparse
import subprocess
import shutil
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional


class ExperimentRunner:
    """实验流水线Execute器"""
    
    def __init__(self, scheme_file: str, verbose: bool = False):
        """
        Initialize实验Execute器
        
        Args:
            scheme_file: JSONConfigurationFilePath
            verbose: YesNoDisplayDetailedOutput
        """
        self.scheme_file = scheme_file
        self.verbose = verbose
        self.base_dir = Path(__file__).parent
        self.scheme = None
        self.experiment_dir = None
        self.results = []
        
    def load_scheme(self) -> bool:
        """Load实验方案Configuration"""
        try:
            with open(self.scheme_file, 'r', encoding='utf-8') as f:
                self.scheme = json.load(f)
            
            print(f"\n{'='*70}")
            print(f"  实验方案: {self.scheme.get('experiment_name', 'Unknown')}")
            print(f"{'='*70}")
            print(f"Description: {self.scheme.get('description', 'N/A')}")
            print(f"Algorithm数量: {len(self.scheme.get('algorithms', []))}")
            
            # CreateOutput directory
            output_dir = self.scheme.get('output_dir', 'results/experiment')
            self.experiment_dir = self.base_dir / output_dir
            self.experiment_dir.mkdir(parents=True, exist_ok=True)
            
            print(f"Output directory: {self.experiment_dir}")
            print(f"{'='*70}\n")
            
            return True
            
        except FileNotFoundError:
            print(f"Error: ConfigurationFileNot存In: {self.scheme_file}")
            return False
        except json.JSONDecodeError as e:
            print(f"Error: JSONFormatError: {e}")
            return False
        except Exception as e:
            print(f"Error: LoadConfigurationFailed: {e}")
            return False
    
    def run_collect_traces(self, algo_config: Dict[str, Any]) -> Optional[str]:
        """
        ExecuteTrace采集
        
        Args:
            algo_config: AlgorithmConfigurationDictionary
            
        Returns:
            TraceFilePath，FailedReturnNone
        """
        algorithm = algo_config['name']
        collect_cfg = algo_config.get('collect_config', {})
        
        print(f"\n[Step1] 采集 {algorithm.upper()} Trace...")
        
        # 构建Command
        cmd = [
            'python', 'collect_firmware_traces.py',
            '-a', collect_cfg.get('algorithm', algorithm),
            '-n', str(collect_cfg.get('traces', algo_config.get('num_traces', 100)))
        ]
        
        # AddOptional parameters（Filter掉注释字段AndNone值）
        def add_param(key, flag, value_func=str):
            if key in collect_cfg and not key.startswith('_') and collect_cfg[key] is not None:
                cmd.extend([flag, value_func(collect_cfg[key])])
        
        add_param('key', '--key')
        add_param('delay', '--delay', str)
        add_param('work_dir', '--work-dir')
        
        if collect_cfg.get('no_key', False):
            cmd.append('--no-key')
        if collect_cfg.get('no_plaintexts', False):
            cmd.append('--no-plaintexts')
        
        # 指定Output file
        if 'output' in collect_cfg and collect_cfg['output']:
            trace_file = collect_cfg['output']
            cmd.extend(['--output', trace_file])
        else:
            trace_file = f"{algorithm}_traces.npz"
        
        try:
            print(f"  ExecuteCommand: {' '.join(cmd)}")
            result = subprocess.run(
                cmd,
                cwd=self.base_dir,
                capture_output=not self.verbose,
                text=True,
                timeout=3600  # 1小时超时
            )
            
            if result.returncode == 0:
                trace_path = self.base_dir / trace_file
                if trace_path.exists():
                    print(f"  Success: Trace已SaveTo {trace_file}")
                    return str(trace_path)
                else:
                    print(f"  Warning: CommandExecuteSuccess但未找ToOutput file")
                    return None
            else:
                print(f"  Failed: 采集Failed (Return码: {result.returncode})")
                if not self.verbose and result.stderr:
                    print(f"  ErrorInformation: {result.stderr[:200]}")
                return None
                
        except subprocess.TimeoutExpired:
            print(f"  Error: 采集超时")
            return None
        except Exception as e:
            print(f"  Error: {e}")
            return None
    
    def run_preprocess(self, trace_file: str, preprocess_cfg: Dict[str, Any]) -> Optional[str]:
        """
        ExecuteTrace预Process（Optional）
        
        Args:
            trace_file: InputTraceFile
            preprocess_cfg: 预ProcessConfiguration
            
        Returns:
            预Processed的TraceFilePath
        """
        if not preprocess_cfg.get('enable', False):
            print(f"\n[Step2] Skip预Process")
            return trace_file
        
        print(f"\n[Step2] 预ProcessTrace...")
        
        # 构建Command
        output_file = preprocess_cfg.get('output') or trace_file.replace('.npz', '_preprocessed.npz')
        cmd = ['python', 'preprocess_traces.py', trace_file, '-o', output_file]
        
        # AddAll预ProcessParameters（Filter注释字段）
        for key, value in preprocess_cfg.items():
            if key.startswith('_') or key in ['enable', 'output']:
                continue
            
            flag = '--' + key.replace('_', '-')
            
            if isinstance(value, bool):
                if value:  # 只HasForTrue时才Add标志
                    cmd.append(flag)
            elif value is not None:
                if key == 'window' and isinstance(value, str):
                    cmd.extend([flag, value])
                else:
                    cmd.extend([flag, str(value)])
        
        try:
            print(f"  ExecuteCommand: {' '.join(cmd)}")
            result = subprocess.run(
                cmd,
                cwd=self.base_dir,
                capture_output=not self.verbose,
                text=True,
                timeout=1800
            )
            
            if result.returncode == 0:
                if Path(output_file).exists():
                    print(f"  Success: 预ProcessCompleted")
                    return output_file
                else:
                    print(f"  Warning: Use原始Trace")
                    return trace_file
            else:
                print(f"  Failed: 预ProcessFailed，Use原始Trace")
                return trace_file
                
        except Exception as e:
            print(f"  Error: {e}，Use原始Trace")
            return trace_file
    
    def run_analysis(self, trace_file: str, algorithm: str, 
                    analysis_cfg: Dict[str, Any], config_suffix: str = '') -> Optional[str]:
        """
        ExecuteAttackAnalyze
        
        Args:
            trace_file: TraceFilePath
            algorithm: Algorithm名称
            analysis_cfg: AnalyzeConfiguration
            config_suffix: Configuration后缀（用于区分MultipleConfiguration）
            
        Returns:
            报告FilePath
        """
        print(f"\n[Step3] ExecuteAttackAnalyze...")
        
        # Generate报告File名
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        
        # Process报告FileConfiguration
        report_setting = analysis_cfg.get('report', 'auto')
        if report_setting == 'auto' or report_setting is True:
            # AddConfiguration名称ToFile名
            cfg_name = analysis_cfg.get('name', '')
            if cfg_name:
                report_file = self.experiment_dir / f"{algorithm}_{cfg_name}_{timestamp}_report.json"
            elif config_suffix:
                report_file = self.experiment_dir / f"{algorithm}_{config_suffix}_{timestamp}_report.json"
            else:
                report_file = self.experiment_dir / f"{algorithm}_{timestamp}_report.json"
        elif report_setting:
            report_file = Path(report_setting)
        else:
            report_file = None
        
        # 构建Command
        cmd = [
            'python', 'analyze_firmware_traces.py',
            trace_file,
            '--algorithm', analysis_cfg.get('algorithm', algorithm),
            '--attacks', *analysis_cfg.get('attacks', ['cpa']),
            '--leakage-model', analysis_cfg.get('leakage_model', 'hamming_weight')
        ]
        
        # Add报告Parameters
        if report_file:
            cmd.extend(['--report', str(report_file)])
        
        # AddAllAnalyzeParameters（Filter注释字段）
        param_mapping = {
            'plaintexts': '--plaintexts',
            'target_operation': '--target-operation',
            'top_n': '--top-n',
            'save_result': '--save-result',
            'log_level': '--log-level'
        }
        
        for key, flag in param_mapping.items():
            if key in analysis_cfg and analysis_cfg[key] is not None and not str(analysis_cfg[key]).startswith('_'):
                cmd.extend([flag, str(analysis_cfg[key])])
        
        # Process字节范围
        if 'byte_range' in analysis_cfg and analysis_cfg['byte_range']:
            byte_range = analysis_cfg['byte_range']
            cmd.extend(['--byte-range', f"{byte_range[0]},{byte_range[1]}"])
        
        # ProcessBoolean标志
        if analysis_cfg.get('verbose', False):
            cmd.append('--verbose')
        
        try:
            print(f"  ExecuteCommand: {' '.join(cmd)}")
            result = subprocess.run(
                cmd,
                cwd=self.base_dir,
                capture_output=not self.verbose,
                text=True,
                timeout=7200  # 2小时超时
            )
            
            if result.returncode == 0 and report_file.exists():
                print(f"  Success: 报告已Generate")
                return str(report_file)
            else:
                print(f"  Failed: AnalyzeFailed")
                return None
                
        except Exception as e:
            print(f"  Error: {e}")
            return None
    
    def run_plot(self, trace_file: str, plot_cfg: Dict[str, Any]):
        """
        ExecuteTrace可视化（Optional）
        
        Args:
            trace_file: TraceFilePath
            plot_cfg: 绘图Configuration
        """
        print(f"\n[Step4] GenerateTrace可视化...")
        
        # 构建Command
        cmd = ['python', 'plot_traces.py', trace_file]
        
        # AddOutput directory
        if 'output' in plot_cfg and plot_cfg['output']:
            output_dir = plot_cfg['output']
        else:
            output_dir = str(self.experiment_dir / 'plots')
        
        cmd.extend(['--output', output_dir])
        
        try:
            print(f"  ExecuteCommand: {' '.join(cmd)}")
            result = subprocess.run(
                cmd,
                cwd=self.base_dir,
                capture_output=True,
                text=True,
                timeout=600
            )
            
            if result.returncode == 0:
                print(f"  Success: 图像已SaveTo {output_dir}")
            else:
                print(f"  Warning: 绘图Failed")
                
        except Exception as e:
            print(f"  Warning: 绘图异常: {e}")
    
    def run_algorithm_experiment(self, algo_config: Dict[str, Any], 
                                algo_index: int) -> Dict[str, Any]:
        """
        ExecuteSingleAlgorithm的Complete实验流程
        
        Args:
            algo_config: AlgorithmConfiguration
            algo_index: Algorithm索引
            
        Returns:
            实验ResultDictionary
        """
        algorithm = algo_config['name']
        result = {
            'algorithm': algorithm,
            'success': False,
            'trace_file': None,
            'report_file': None,
            'error': None
        }
        
        print(f"\n{'*'*70}")
        print(f"  实验 {algo_index + 1}: {algorithm.upper()}")
        print(f"{'*'*70}")
        
        try:
            # Step1: 采集Trace
            trace_file = self.run_collect_traces(algo_config)
            if not trace_file:
                result['error'] = 'Trace采集Failed'
                return result
            
            result['trace_file'] = trace_file
            
            # Step2: 预Process（Optional）
            preprocess_cfg = algo_config.get('preprocess_config', {})
            processed_trace = self.run_preprocess(trace_file, preprocess_cfg)
            
            # 复制TraceFileToResultDirectory（提前复制）
            trace_copy = self.experiment_dir / Path(processed_trace).name
            shutil.copy2(processed_trace, trace_copy)
            print(f"\n  TraceFile已复制To: {trace_copy}")
            
            # Step3: AttackAnalyze（SupportMultipleConfiguration）
            # Support两种Format：analysis_config（Single）Or analysis_configs（Multiple）
            analysis_configs = []
            if 'analysis_configs' in algo_config:
                # 新Format：MultipleAnalyzeConfiguration
                analysis_configs = algo_config['analysis_configs']
                print(f"\n  检测To {len(analysis_configs)} 个AnalyzeConfiguration")
            elif 'analysis_config' in algo_config:
                # 旧Format：SingleAnalyzeConfiguration
                analysis_configs = [algo_config['analysis_config']]
            else:
                # DefaultConfiguration
                analysis_configs = [{}]
            
            # ExecuteAllAnalyzeConfiguration
            report_files = []
            for idx, analysis_cfg in enumerate(analysis_configs, 1):
                if len(analysis_configs) > 1:
                    print(f"\n  --- AnalyzeConfiguration {idx}/{len(analysis_configs)} ---")
                    cfg_name = analysis_cfg.get('name', f'config_{idx}')
                    print(f"  Configuration名称: {cfg_name}")
                
                # GenerateConfiguration后缀
                config_suffix = f"cfg{idx}" if len(analysis_configs) > 1 else ''
                report_file = self.run_analysis(processed_trace, algorithm, analysis_cfg, config_suffix)
                
                if report_file:
                    report_files.append(report_file)
                else:
                    print(f"  Warning: AnalyzeConfiguration {idx} Failed")
            
            # 判断Success
            if report_files:
                result['report_file'] = report_files[0] if len(report_files) == 1 else report_files
                result['report_files'] = report_files
                result['success'] = True
                
                # Step4: 绘图（Optional）
                plot_cfg = algo_config.get('plot_config', {})
                if plot_cfg.get('enable', False):
                    self.run_plot(processed_trace, plot_cfg)
            else:
                result['error'] = 'AllAttackAnalyze均Failed'
            
        except Exception as e:
            result['error'] = str(e)
            print(f"\n  实验异常: {e}")
        
        return result
    
    def generate_summary(self):
        """Generate实验总结报告"""
        print(f"\n{'='*70}")
        print(f"  实验总结")
        print(f"{'='*70}")
        
        summary = {
            'experiment_name': self.scheme.get('experiment_name', 'Unknown'),
            'scheme_file': self.scheme_file,
            'execution_time': datetime.now().isoformat(),
            'output_directory': str(self.experiment_dir),
            'total_algorithms': len(self.results),
            'successful_algorithms': sum(1 for r in self.results if r['success']),
            'results': self.results
        }
        
        # Save总结报告
        summary_file = self.experiment_dir / 'experiment_summary.json'
        with open(summary_file, 'w', encoding='utf-8') as f:
            json.dump(summary, f, indent=2, ensure_ascii=False)
        
        print(f"\n总Algorithm数: {summary['total_algorithms']}")
        print(f"Success: {summary['successful_algorithms']}")
        print(f"Failed: {summary['total_algorithms'] - summary['successful_algorithms']}")
        
        print(f"\nDetailedResult:")
        for i, result in enumerate(self.results, 1):
            status = "Success" if result['success'] else "Failed"
            print(f"  {i}. {result['algorithm'].upper()}: {status}")
            if result['success']:
                # ProcessSingleOrMultiple报告File
                report_file = result['report_file']
                if isinstance(report_file, list):
                    print(f"     报告数量: {len(report_file)}")
                    for idx, rf in enumerate(report_file, 1):
                        print(f"       {idx}. {Path(rf).name}")
                else:
                    print(f"     报告: {Path(report_file).name}")
            else:
                print(f"     原因: {result.get('error', '未知')}")
        
        print(f"\n总结报告: {summary_file}")
        print(f"AllFile已SaveTo: {self.experiment_dir}")
        print(f"{'='*70}\n")
    
    def run(self) -> bool:
        """ExecuteComplete实验流程"""
        # LoadConfiguration
        if not self.load_scheme():
            return False
        
        # ExecuteEachAlgorithm的实验
        algorithms = self.scheme.get('algorithms', [])
        if not algorithms:
            print("Error: Configuration中没HasAlgorithm定义")
            return False
        
        for i, algo_config in enumerate(algorithms):
            result = self.run_algorithm_experiment(algo_config, i)
            self.results.append(result)
        
        # Generate总结
        self.generate_summary()
        
        # 判断整体YesNoSuccess
        success_count = sum(1 for r in self.results if r['success'])
        return success_count > 0


def main():
    parser = argparse.ArgumentParser(
        description='Side-Channel Attack实验流水线ExecuteScript',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Example:
  # ExecuteSingle实验方案
  python run_experiment.py scheme/aes_cpa_experiment.json
  
  # Execute并DisplayDetailedOutput
  python run_experiment.py scheme/multi_algorithm_comparison.json --verbose
  
  # 列出All可用的实验方案
  python run_experiment.py --list
        """)
    
    parser.add_argument('scheme_file', type=str, nargs='?',
                       help='实验方案JSONConfigurationFilePath')
    
    parser.add_argument('--verbose', '-v', action='store_true',
                       help='DisplayDetailedOutput')
    
    parser.add_argument('--list', '-l', action='store_true',
                       help='列出schemeDirectory中的All方案')
    
    args = parser.parse_args()
    
    # 列出All方案
    if args.list:
        scheme_dir = Path(__file__).parent / 'scheme'
        if not scheme_dir.exists():
            print("schemeDirectoryNot存In")
            return 1
        
        schemes = list(scheme_dir.glob('*.json'))
        if not schemes:
            print("schemeDirectory中没Has找ToJSONConfigurationFile")
            return 1
        
        print(f"\n可用的实验方案 ({len(schemes)}个):")
        print("="*70)
        for scheme in sorted(schemes):
            try:
                with open(scheme, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                name = config.get('experiment_name', 'Unknown')
                desc = config.get('description', 'N/A')
                algos = len(config.get('algorithms', []))
                print(f"\nFile: {scheme.name}")
                print(f"  名称: {name}")
                print(f"  Description: {desc}")
                print(f"  Algorithm: {algos}个")
            except:
                print(f"\nFile: {scheme.name} (FormatError)")
        print("\n" + "="*70)
        return 0
    
    # CheckRequired parameters
    if not args.scheme_file:
        parser.print_help()
        return 1
    
    # Execute实验
    runner = ExperimentRunner(args.scheme_file, verbose=args.verbose)
    success = runner.run()
    
    return 0 if success else 1


if __name__ == '__main__':
    sys.exit(main())
